package com.zed.api.common.resource;

import cn.hutool.core.io.IoUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;


/**
 * 资源提取器
 *
 * @author Zed
 */
public class ResourcesExtractor {

    private static final String PATH = "HNE-INF";
    private Logger logger;
    private List<ResourceScanner<?>> scanners;

    /**
     * 构造方法
     */
    public ResourcesExtractor() {
        this.logger = LoggerFactory.getLogger(ResourcesExtractor.class.getName());
        this.scanners = new ArrayList<ResourceScanner<?>>();
        this.scanners.add(new JarFileResourceScanner());
        this.scanners.add(new TargetClassesResourceScanner());
    }

    /**
     * 提取类路径HNE-INF目录下文件并复制到{@code outputDir}目录下
     *
     * @param classLoader {@link ClassLoader},作为扫描参考路径的url.
     * @param outputDir   {@link File}, 提取复制输出目录
     * @return {@link List}, 复制文件的URL列表
     */
    public List<URL> extract(ClassLoader classLoader, File outputDir) {
        Callback callback = new Callback(outputDir, this.logger);

        try {
            Enumeration<URL> em = classLoader.getResources(PATH);
            while (em.hasMoreElements()) {
                URL url = em.nextElement();
                for (ResourceScanner<?> scanner : this.scanners) {
                    this.extract0(url, PATH, scanner, callback);
                }
            }
        } catch (Exception e) {
            if (logger.isErrorEnabled()) {
                logger.error(e.getMessage(), e);
            }
        }
        return callback.getTargetUrls();
    }

    /**
     * 提取类路径HNE-INF目录下文件并回调
     *
     * @param url      {@link URL},作为扫描参考路径的url.
     * @param callback {@link ResourceScanningCallback}, 扫描后回调
     * @return {@link List}, 复制文件的URL列表
     */
    public List<URL> extract(URL url, ResourceScanningCallback callback) {
        CallbackRecordWrapper wrapper = new CallbackRecordWrapper(callback);
        try {

            for (ResourceScanner<?> scanner : this.scanners) {
                this.extract0(url, PATH, scanner, callback);
            }
        } catch (Exception e) {
            if (logger.isErrorEnabled()) {
                logger.error(e.getMessage(), e);
            }
        }
        return wrapper.getTargetUrls();
    }

    /**
     * 判断{@code scanner}是否支持资源的扫描，如果支持则调用{@code callback}返回扫描结果
     *
     * @param <T>
     * @param url      {@link URL},参考路径的url.
     * @param path     {@link String}, 相对路径
     * @param scanner  {@link ResourceScanner}，扫描器
     * @param callback {@link ResourceScanningCallback}, 回调方法
     */
    protected <T> void extract0(URL url, String path,
                                ResourceScanner<T> scanner, ResourceScanningCallback callback) {
        T instance = scanner.support(url);
        if (instance != null) {
            scanner.scanning(instance, path, (fileName) -> true, true, callback);
        }
    }

    private static class CallbackRecordWrapper implements ResourceScanningCallback {

        private ResourceScanningCallback resourceScanningCallback;

        private List<URL> urls;

        private CallbackRecordWrapper(ResourceScanningCallback resourceScanningCallback) {
            this.resourceScanningCallback = resourceScanningCallback;
            this.urls = new ArrayList<>();
        }


        @Override
        public void onFound(String path, URL resource) {
            this.urls.add(resource);
            this.resourceScanningCallback.onFound(path, resource);
        }

        public List<URL> getTargetUrls() {
            return this.urls;
        }
    }

    private class Callback implements ResourceScanningCallback {
        private final File outputDir;
        private final Logger logger;
        private List<URL> urls;

        public Callback(File outputDir, Logger logger) {
            this.outputDir = Objects.requireNonNull(outputDir, "\"outputDir\" cannot be null.");
            if (this.outputDir.isFile()) {
                throw new IllegalArgumentException("\"outputDir\" cannot be a file.");
            }

            if (!this.outputDir.exists()) {
                this.outputDir.mkdirs();
            }

            this.logger = Objects.requireNonNull(logger, "\"logger\" can；not be null.");
            this.urls = new ArrayList<URL>();
        }

        @Override
        public void onFound(String path, URL resource) {
            if (logger.isTraceEnabled()) {
                logger.trace(String.format("found embed resource : \r\n\tname=%s , \r\n\turl= %s .", path, resource));
            }
            if (path.endsWith("/")) {
                return;
            }

            File outputFile = new File(outputDir, path);
            if (!outputFile.exists()) {
                File dir = outputFile.getParentFile();
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                InputStream input = null;
                try {
                    input = resource.openStream();
                    Path target = outputFile.toPath();
                    Files.copy(input, target);
                    this.urls.add(target.toUri().toURL());
                } catch (IOException e) {
                    if (logger.isErrorEnabled()) {
                        logger.error(e.getMessage(), e);
                    }
                } finally {
                    IoUtil.close(input);
                }
            } else {
                try {
                    this.urls.add(outputFile.toURI().toURL());
                } catch (Exception e) {

                }
            }
        }

        public List<URL> getTargetUrls() {
            return this.urls;
        }
    }

}
